Github
Postsnextauth routing

auth routing

권한에 따른 페이지 접근 제어

개요

프론트에서 권한에 따른 페이지 접근을 제어하기 위한 라우팅을 어떻게 구현하고 관리할 것인지 고민

시도 1. (middleware)

Next.js에서 제공하는 middleware를 사용해서 권한에 따른 리다이렉션

상세 구현 내용

  • Enablement Activation을 확인하는 API를 콜해서 응답받은 데이터를 사용

  • isLogin, isActivated 변수를 조건으로 상황에 따른 리다이렉트

middleware.ts

1// check하고 하는 request url 2export const config = { 3 matcher: ["/", "/control/:path*", "/enablement/:path*", "/login/:path*", "/manage/:path*"], 4}; 5 6export default async function middleware(request: NextRequest) { 7 ... 8 9 // 1. 쿠키에 session_id가 없으면 바로 로그인 페이지로 리다이렉트 10 if (!cookie || !cookie.includes("session_id")) { 11 if (request.nextUrl.pathname.startsWith("/login")) { 12 return NextResponse.next(); 13 } 14 return NextResponse.redirect(new URL("/login", request.url)); 15 } 16 17 try { 18 const { code, result } = await getActivation(cookie || ""); 19 const isLogin = ![401001].includes(code); 20 const isActivated = result["is_activated"]; 21 22 // 2. login 페이지로 접근했을 때 23 if (request.nextUrl.pathname.startsWith("/login")) { 24 if (isLogin && isActivated) { 25 return NextResponse.redirect(new URL("/", request.url)); 26 } 27 28 if (isLogin && !isActivated) { 29 const url = getRedirectURL("/enablement/license", request.url, "/"); 30 return NextResponse.redirect(url); 31 } 32 33 if (!isLogin && isActivated) { 34 return NextResponse.next(); 35 } 36 37 if (!isLogin && !isActivated) { 38 return NextResponse.next(); 39 } 40 } 41 42 // 3. enablement 페이지로 접근했을 때 43 if (request.nextUrl.pathname.startsWith("/enablement")) { 44 if (isLogin && isActivated) { 45 return NextResponse.redirect(new URL("/", request.url)); 46 } 47 48 if (isLogin && !isActivated) { 49 return NextResponse.next(); 50 } 51 52 if (!isLogin && isActivated) { 53 const url = getRedirectURL("/login", request.url, "/"); 54 return NextResponse.redirect(url); 55 } 56 57 if (!isLogin && !isActivated) { 58 const url = getRedirectURL("/login", request.url, "/enablement/license"); 59 return NextResponse.redirect(url); 60 } 61 } 62 63 // 4. 그 외 페이지로 접근했을 때 64 if (isLogin && isActivated) { 65 return NextResponse.next(); 66 } 67 68 if (isLogin && !isActivated) { 69 const url = getRedirectURL("/enablement/license", request.url, "/"); 70 return NextResponse.redirect(url); 71 } 72 73 if (!isLogin && isActivated) { 74 const url = getRedirectURL("/login", request.url, "/"); 75 return NextResponse.redirect(url); 76 } 77 78 if (!isLogin && !isActivated) { 79 const url = getRedirectURL("/login", request.url, "/enablement/license"); 80 return NextResponse.redirect(url); 81 } 82 } catch (e) { 83 // 5. 에러가 발생한 경우 에러 페이지로 리다이렉트 84 const error = e as CustomError; 85 86 if (error.data) { 87 return NextResponse.redirect( 88 new URL( 89 `/_error?errorCode=${error.data.code}&errorMessage=${error.data.message}&redirectURL=${request.nextUrl.pathname}`, 90 request.url 91 ) 92 ); 93 } 94 95 return NextResponse.redirect(new URL(`/_error?errorCode=500&redirectURL=${request.nextUrl.pathname}`, request.url)); 96 }

문제 사항

  1. URL 캐싱 때문에 페이지가 변경될 때마다 실행되지 않는다.

시도 2. (server side)

getServerSideProps 를 사용해서 매 페이지 실행마다 서버에서 리다이렉트

상세 구현 내용

auth-router.ts

1import { type GetServerSidePropsContext, type Redirect } from "next"; 2 3import { getAuthConditions } from "./get-auth-conditions"; 4 5type GetRoute = (request: GetServerSidePropsContext["req"]) => Promise<Response>; 6 7type Response = 8 | { redirect?: Redirect; redirectURL?: string; props: Record<string, never> } 9 | { props: Record<string, never> }; 10 11export const authRouter: GetRoute = async (request) => { 12 const { isLoggedIn, isActivated, error } = await getAuthConditions(); 13 14 if (error) { 15 return response.redirect(`/_error?errorCode=${error.code}&errorMessage=${error.message}`); 16 } 17 18 // 이하 middleware와 조건부 라우팅 로직과 같음 19 // ... 20 21const response = { 22 next: () => ({ 23 props: {}, 24 }), 25 26 redirect: (url: string) => ({ 27 redirect: { 28 destination: url, 29 permanent: false, 30 }, 31 props: {}, 32 }), 33};

page.tsx

1const Page = () => { 2 return ( 3 <></> 4 ); 5}; 6 7export const getServerSideProps: GetServerSideProps = async ({ req }) => { 8 return await authRouter(req); 9}; 10 11export default Page;

문제 사항

  1. 모든 페이지마다 getServerSideProps 를 작성해줘야 하는 번거로움이 있다.
  2. 페이지를 이동할 때마다 서버로 요청이 간다.

다른 방법. (HOC)

페이지 컴포넌트를 감싸는 고차 컴포넌트를 사용해서 이동마다 리다이렉트를 실행

(카카오 페이 증권에서는 해당 방식 사용)

에러 상황에 따라 모달 등 UI 대처의 폭이 넓어진다.

문제 사항

  1. 페이지가 로드되고 리다이렉트가 발생하므로 잠시 해당 페이지가 보여서 UX 적으로 좋지 않다.

    (flickering issue?)

문제 개선 방법

  • HOC에서 로직을 수행하는 지연 시간 동안 로딩 UI를 보여준다.

결론

Server Side에서 리다이렉트 시키는 것으로 결정

이유

  1. 깜빡이는 문제 또는 페이지 이동간 로딩 UI를 넣는 것은 UX상 좋지 않음
  2. 페이지 이동마다 서버로 요청이 가는 것은 현재 프로덕트에서 성능상 크게 문제되지 않음